Skip to content

fix(spawn): keep strong ref to fire-and-forget subagent task - #13

Merged
zhao9797 merged 3 commits into
Darwin-Agent:mainfrom
hi-neason:fix/spawn-subagent-task-ref
Jul 29, 2026
Merged

fix(spawn): keep strong ref to fire-and-forget subagent task#13
zhao9797 merged 3 commits into
Darwin-Agent:mainfrom
hi-neason:fix/spawn-subagent-task-ref

Conversation

@hi-neason

Copy link
Copy Markdown
Contributor

Summary

spawn_subagent(wait=False) calls asyncio.create_task(_run_child()) without storing the returned Task (harnessx/tools/spawn_subagent.py:227). Because the asyncio event loop holds only weak references to tasks, an idle interval between the tool return and the child harness's first await allows the task to be garbage-collected — silently dropping the subagent's result and leaving its entry stuck in parent_state.pending_subagents.

For comparison, both harnessx/processors/light_memory/processors.py:242 and harnessx/tools/web_search.py:416 already use the canonical "hold a set of tasks + discard on done" pattern; only this call site was missed.

Change

  • Add module-level _BACKGROUND_TASKS: set[asyncio.Task] alongside _spawn_ctx.
  • Store each spawn task with a stable name (spawn_subagent:<label>) and register a discard done-callback.

Tests

New regression test_spawn_async_task_is_referenced_until_done:

  1. Blocks the fake child on an asyncio.Event so the task is definitely still running when we probe.
  2. Asserts the task is present in _BACKGROUND_TASKS.
  3. Runs gc.collect() and re-asserts (proves the strong ref is holding).
  4. Releases the event and asserts the done-callback removes it.

Full suite: 946 → 947 unit tests passing (same 1 pre-existing macOS unrelated failure).

References

  • asyncio.create_task docs — "Save a reference to the result of this function, to avoid a task disappearing mid-execution."

Checklist

  • pytest tests/unit/ tests/integration/ -q passes (947 passed, 1 pre-existing unrelated failure)
  • No hardcoded API keys / internal addresses
  • No new dependencies
  • Regression test added
  • Conventional commit message

spawn_subagent(wait=False) called asyncio.create_task(_run_child())
without keeping a reference. asyncio only holds weak references to
tasks, so the returned Task could be garbage-collected between the
tool return and the child harness's first await point, silently
dropping the subagent's result and leaving pending_subagents stuck.

Store the task in a module-level _BACKGROUND_TASKS set and discard it
via a done-callback — the canonical pattern documented in the asyncio
create_task() docs. Add a regression test that pins a child task on an
event, GC-collects, and asserts the task remains reachable.

@zhao9797 zhao9797 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the fire-and-forget subagent lifecycle path. Holding the created Task in a module-level set and discarding it on completion matches asyncio's documented pattern, does not affect the synchronous path, and cleans up on both success and failure. The spawn_subagent unit tests pass with the dev async test extra, and I also checked the failure path releases the strong reference.

@zhao9797
zhao9797 merged commit bf5f199 into Darwin-Agent:main Jul 29, 2026
5 checks passed
@hi-neason
hi-neason deleted the fix/spawn-subagent-task-ref branch July 29, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants